/******************************************************************************
	Cobalt - utility libraries for building XNA games
	Copyright (C) 2008 Philip Djonov

	This program is free software; you can redistribute it and/or modify it
	under the terms of the GNU General Public License as published by the Free
	Software Foundation; either version 2 of the License, or (at your option)
	any later version.

	This program is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
	or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
	for more details.

	You should have received a copy of the GNU General Public License along
	with this program; if not, write to the
	
		Free Software Foundation, Inc.
		51 Franklin Street, Fifth Floor
		Boston, MA  02110-1301, USA.
******************************************************************************/

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;

namespace Cobalt.Content.Pipeline.Quake.Bsp
{
	public partial class BspImporter
	{
		public BspImporter() { }

		private Shaders.ShaderParser shaderParser = new Shaders.ShaderParser();

		public ImportedBspContent Import( string filename )
		{
			if( fs == null )
				throw new InvalidOperationException();

			shaderParser.Logger = logger;

			LoadBspData( filename );

			content = new ImportedBspContent();
			importedShaders = new Dictionary<Helpers.Pair<int, int>, ImportedMaterialContent>();

			content.Identity = identity;

			ImportEntities();
			ImportLightmaps();
			ImportShaders();
			ImportSurfaces();
			ImportBrushModels();
			ImportPvs();
			ImportNodes();
			SaveMaterialList();

			ImportedBspContent ret = content;

			importedShaders = null;
			content = null;
			UnloadBspData();

			return ret;	
		}

		private ContentBuildLogger logger = NullBuildLogger.NullLogger;
		public ContentBuildLogger Logger
		{
			get { return logger; }
			set { logger = value ?? NullBuildLogger.NullLogger; }
		}

		private QuakeFs fs;
		public QuakeFs Fs
		{
			get { return fs; }
			set { fs = value; }
		}

		private ImportedBspContent content;

		private Dictionary<Helpers.Pair<int, int>, ImportedMaterialContent> importedShaders;

		private void ParseEntityData( ContentTokenReader reader )
		{
			int startEntCount = content.Entities.Count;

			while( reader.ReadToken() != null )
			{
				if( reader.LastToken != "{" )
					throw new InvalidContentException( "Error parsing entity string, expected '{'.",
						reader.LastTokenIdentity );

				ImportedEntityContent ent = new ImportedEntityContent();
				ent.Identity = reader.LastTokenIdentity;

				while( reader.ReadRequiredToken() != null )
				{
					if( reader.LastToken == "}" )
						break;

					string name = reader.LastToken;
					string value = reader.ReadRequiredToken();

					ent.Values[name] = value;
				}

				if( content.Entities.Count == 0 )
				{
					//BSP entities are always to be read *before* external entities,
					//so the ContentIdentity should be valid for this

					//first one must be the world spawn
					if( ent.ClassName != EntityClasses.WorldSpawn )
						throw new InvalidContentException( "Missing worldspawn entity.", identity );
				}

				content.Entities.Add( ent );
			}

			Logger.LogMessage( "Imported {0} entities.", content.Entities.Count - startEntCount );
		}

		private void ParseEntityData( string entityString, ContentIdentity sourceIdentity )
		{
			using( ContentTokenReader reader = new ContentTokenReader( entityString, sourceIdentity ) )
				ParseEntityData( reader );
		}

		private void ImportEntities()
		{
			ParseEntityData( LoadEntityString(), identity );

			if( content.Entities.Count == 0 )
				throw new InvalidContentException( "Missing entities.", identity );
		}

		#region Shaders
		private Dictionary<string, ImportedShaderContent> ImportShaderScripts()
		{
			Dictionary<string, ImportedShaderContent> ret = new Dictionary<string, ImportedShaderContent>();

			foreach( string shaderFile in fs.GetFiles( "scripts", "*.shader" ) )
			{
				try
				{
					ImportShadersInScript( ret, shaderFile );
					Logger.LogMessage( "Imported shader file '{0}'.", shaderFile );
				}
				catch( InvalidContentException ex )
				{
					Logger.LogWarning( string.Empty, ex.ContentIdentity, ex.Message );
				}
				//catch
				//{
				//	context.Logger.LogWarning( string.Empty, new ContentIdentity( shaderFile ), "Error importing shaders." );
				//}
			}

			return ret;
		}

		private string QuoteIfNecessary( string s )
		{
			bool needsQuotes = false;
			char anyQuote = '\0';

			foreach( char c in s )
			{
				if( c == '\'' || c == '\"' )
					anyQuote = c;

				if( c < ' ' )
					needsQuotes = true;
			}

			if( needsQuotes )
			{
				char quote = '\"';
				if( quote == anyQuote )
					quote = '\'';

				return quote.ToString() + s + quote.ToString();
			}

			return s;
		}

		private void ImportShadersInScript( Dictionary<string, ImportedShaderContent> shaderList, string filename )
		{
			string fullPath;
			using( Stream scriptStream = fs.OpenPath( filename, out fullPath ) )
			using( ContentTokenReader reader = new ContentTokenReader( new StreamReader( scriptStream ).ReadToEnd(),
				new ContentIdentity( fullPath, string.Empty, string.Empty ), new QuakeTokenizer() ) )
			{
				for( ; ; )
				{
					string name = reader.ReadToken( TokenReadFlags.RequireLineStart );

					if( name == null )
						break;

					reader.ReadExactToken( "{" );

					ImportedShaderContent shader = new ImportedShaderContent();
					shader.Name = name;
					shader.Identity = reader.LastTokenIdentity;

					StringBuilder sb = new StringBuilder();

					int depth = 1;
					for( ; ; )
					{
						string tok = reader.ReadRequiredToken();

						if( tok == "}" )
						{
							if( --depth == 0 )
								break;
						}

						if( tok == "{" )
							depth++;

						sb.Append( QuoteIfNecessary( tok ) );

						if( !reader.HasMoreTokensInCurrentLine )
							//just ran a line out
							sb.Append( '\n' );

						sb.Append( ' ' );
					}

					shader.ShaderText = sb.ToString();
					shaderList[name] = shader;

					Logger.LogMessage( "Imported '{0}'.", name );
				}
			}
		}

		private void ImportShaders()
		{
			Dictionary<string, ImportedShaderContent> textShaders = ImportShaderScripts();

			foreach( DShader inShader in LoadDShaders() )
			{
				ImportedShaderContent shader = new ImportedShaderContent();
				shader.Identity = content.Identity;

				shader.Name = inShader.ShaderName;
				shader.ContentFlags = inShader.ContentFlags;
				shader.SurfaceFlags = inShader.SurfaceFlags;

				ImportedShaderContent textShader;
				if( textShaders.TryGetValue( inShader.ShaderName, out textShader ) )
				{
					shader.Identity = textShader.Identity;
					shader.ShaderText = textShader.ShaderText;
				}

				content.Shaders.Add( shader );
			}

			Logger.LogMessage( "Imported {0} shaders.", content.Shaders.Count );
		}

		private ImportedMaterialContent ImportShader( int shaderIndex, int lightmapIndex )
		{
			if( shaderIndex < 0 )
				shaderIndex = 0;
			if( shaderIndex >= content.Shaders.Count )
				shaderIndex = content.Shaders.Count - 1;

			if( content.Deluxemaps.Count != 0 )
				lightmapIndex /= 2;
			
			if( lightmapIndex >= content.Lightmaps.Count )
				lightmapIndex = content.Lightmaps.Count - 1;

			Helpers.Pair<int, int> id = new Helpers.Pair<int, int>( shaderIndex, lightmapIndex );

			ImportedMaterialContent ret;
			if( !importedShaders.TryGetValue( id, out ret ) )
			{
				var shader = content.Shaders[shaderIndex];

				ret = new ImportedMaterialContent();
				ret.Name = shader.Name;
				ret.Identity = shader.Identity;
				ret.Shader = shader;
				ret.Lightmap = lightmapIndex >= 0 ? content.Lightmaps[lightmapIndex] : null;				

				importedShaders.Add( id, ret );
			}

			return ret;
		}

		private static string[] imageExtensions = { ".tga", ".jpg", ".bmp", ".dds" };
		public string GetImagePath( string path )
		{
			foreach( string ext in imageExtensions )
			{
				string imgPath = path + ext;
				
				string osPath = fs.GetOsPath( imgPath );
				if( osPath != null )
					return osPath;
			}

			return null;
		}

		private void ParseShaderTexts()
		{
			foreach( var material in content.Materials )
			{
				if( !string.IsNullOrEmpty( material.Shader.ShaderText ) )
				{
					//handle explicit shaders

					using( ContentTokenReader reader = new ContentTokenReader( material.Shader.ShaderText, material.Shader.Identity, new QuakeTokenizer() ) )
						material.ParsedShader = shaderParser.ParseShaderContents( material.Shader.Name, reader );

					foreach( var sampler in Shaders.ShaderCompiler.GetBundleTextures( material.ParsedShader ).InvertMapping() )
					{
						if( sampler.Key == "Lightmap" )
							continue;

						string path = Path.ChangeExtension( sampler.Value.Images[0], null );
						
						if( sampler.Value is Shaders.SkyTextureBundle )
							path += "_up";

						path = GetImagePath( path );

						if( path != null )
							material.Textures[sampler.Key] = new ExternalReference<TextureContent>( path );
					}
				}
				else
				{
					//handle implicit shaders
					if( material.Lightmap != null )
					{
						material.ParsedShader = Shaders.Shader.DefaultLightmappedShader;
					}
					else
					{
						material.ParsedShader = Shaders.Shader.DefaultVertexLitShader;
					}

					material.Textures["Diffuse"] = new ExternalReference<TextureContent>( GetImagePath( Path.ChangeExtension( material.Shader.Name, null ) ) );
					//lightmap reference is injected at process time, if appropriate, of course
				}
			}
		}

		private void SaveMaterialList()
		{
			content.Materials.AddRange( importedShaders.Values );
			ParseShaderTexts();
		}
		#endregion

		#region Surfaces
		private void ParseTriangles( GeometryContent gc, DSurface surf, List<DrawVert> vertices, List<int> indices )
		{
			int baseVert = gc.Parent.Positions.Count;
			for( int i = 0; i < surf.VertexCount; i++ )
			{
				gc.Parent.Positions.Add( vertices[surf.FirstVertex + i].Position );
				gc.Vertices.Add( baseVert + i );
			}

			VertexChannel<Vector2> tcChan;

			tcChan = gc.Vertices.Channels.Add<Vector2>( VertexChannelNames.TextureCoordinate( 0 ), null );
			for( int i = 0; i < surf.VertexCount; i++ )
				tcChan[i] = vertices[surf.FirstVertex + i].TextureCoord;

			tcChan = gc.Vertices.Channels.Add<Vector2>( VertexChannelNames.TextureCoordinate( 1 ), null );
			for( int i = 0; i < surf.VertexCount; i++ )
				tcChan[i] = vertices[surf.FirstVertex + i].LightmapCoord;

			VertexChannel<Vector3> normChan = gc.Vertices.Channels.Add<Vector3>( VertexChannelNames.Normal( 0 ), null );
			for( int i = 0; i < surf.VertexCount; i++ )
			{
				normChan[i] = vertices[surf.FirstVertex + i].Normal;
			}

			VertexChannel<Color> clChan = gc.Vertices.Channels.Add<Color>( VertexChannelNames.Color( 0 ), null );
			for( int i = 0; i < surf.VertexCount; i++ )
			{
				clChan[i] = vertices[surf.FirstVertex + i].Color;
			}

			for( int i = 0; i < surf.IndexCount; i++ )
				gc.Indices.Add( indices[surf.FirstIndex + i] );
		}

		private void ParseFace( GeometryContent gc, DSurface surf, List<DrawVert> vertices, List<int> indices )
		{
			ParseTriangles( gc, surf, vertices, indices );
		}

		private void ParsePatch( GeometryContent gc, DSurface surf, List<DrawVert> vertices, List<Patch> patchStaging )
		{
			DrawVert[] verts = new DrawVert[surf.PatchWidth * surf.PatchHeight];
			for( int i = 0; i < verts.Length; i++ )
				verts[i] = vertices[surf.FirstVertex + i];

			Patch patch = Patch.SubdividePatchToGrid( surf.PatchWidth, surf.PatchHeight, verts );
			patch.TargetMesh = gc;

			patchStaging.Add( patch );
		}

		private void SetupEmptyGeometryNode( GeometryContent gc )
		{
			gc.Vertices.Channels.Add<Vector2>( VertexChannelNames.TextureCoordinate( 0 ), null );
			gc.Vertices.Channels.Add<Vector2>( VertexChannelNames.TextureCoordinate( 1 ), null );
			gc.Vertices.Channels.Add<Vector3>( VertexChannelNames.Normal( 0 ), null );
			gc.Vertices.Channels.Add<Color>( VertexChannelNames.Color( 0 ), null );
		}

		private void ImportSurfaces()
		{
			List<DrawVert> vertices = new List<DrawVert>( LoadDrawVerts() );
			List<int> indices = new List<int>( LoadDrawIndices() );

			List<Patch> patchStaging = new List<Patch>();

			int numPatches = 0;
			int numPlanar = 0;
			int numMeshes = 0;

			foreach( DSurface surf in LoadDSurfaces() )
			{
				GeometryContent gc = new GeometryContent();
				content.Mesh.Geometry.Add( gc );

				switch( surf.SurfaceType )
				{
				case SurfaceType.Patch:
					numPatches++;
					ParsePatch( gc, surf, vertices, patchStaging );
					break;

				case SurfaceType.Planar:
					numPlanar++;
					ParseFace( gc, surf, vertices, indices );
					break;

				case SurfaceType.TriangleSoup:
					numMeshes++;
					ParseTriangles( gc, surf, vertices, indices );
					break;

				default:
					SetupEmptyGeometryNode( gc );
					Logger.LogWarning( string.Empty, identity, "Unsupported surface type: {0}.", surf.SurfaceType );
					continue;
				}

				gc.Material = ImportShader( surf.ShaderIndex, surf.LightmapIndex );
			}

			int numStitched = Patch.StitchAllPatches( patchStaging );
			Patch.FixSharedVertexLodError( patchStaging );

			for( int i = 0; i < patchStaging.Count; i++ )
				patchStaging[i].GenerateGeometry();

			Logger.LogMessage( "Imported {0} surfaces: {1} planar surfaces, {2} mesh models, {3} patches.",
				numPatches + numPlanar + numMeshes, numPlanar, numMeshes, numPatches );
			Logger.LogMessage( "Stitched {0} patches.", numStitched );
		}
		#endregion

		#region Lightmaps

		private void ImportLightmaps()
		{
			bool hasDeluxeMaps = false;
			string deluxeKeyVal;
			if( content.Entities[0].Values.TryGetValue( EntityKeys.DeluxeMapping, out deluxeKeyVal ) )
			{
				int kVal;
				if( int.TryParse( deluxeKeyVal, out kVal ) )
					hasDeluxeMaps = kVal != 0;
			}

			int i = 0;
			foreach( PixelBitmapContent<Color> pixmap in LoadLightmaps() )
			{
				Texture2DContent image = new Texture2DContent();
				image.Mipmaps.Add( pixmap );

				if( hasDeluxeMaps && (i & 1) != 0 )
					content.Deluxemaps.Add( image );
				else
					content.Lightmaps.Add( image );

				i++;
			}

			content.RescaleLightmaps = true;

			Logger.LogMessage( "Imported {0} lightmaps and {1} deluxemaps.", content.Lightmaps.Count, content.Deluxemaps.Count );
		}
		#endregion

		private void ImportBrushModels()
		{
			foreach( DModel m in LoadDModels() )
			{
				ImportedBrushModelContent retModel = new ImportedBrushModelContent();

				for( int i = 0; i < m.NumSurfaces; i++ )
					retModel.Surfaces.Add( content.Mesh.Geometry[m.FirstSurface + i] );

				content.BrushModels.Add( retModel );
			}
		}

		private void ImportNodes()
		{
			List<DShader> shaders = new List<DShader>( LoadDShaders() );
			List<DNode> nodes = new List<DNode>( LoadDNodes() );
			List<DPlane> planes = new List<DPlane>( LoadDPlanes() );
			List<DLeaf> leaves = new List<DLeaf>( LoadDLeafs() );
			List<DBrush> brushes = new List<DBrush>( LoadDBrushes() );
			List<int> leafBrushes = new List<int>( LoadLeafBrushes() );
			List<DBrushSide> brushSides = new List<DBrushSide>( LoadDBrushSides() );
			List<int> markSurfaces = new List<int>( LoadLeafSurfaces() );

			foreach( DBrush brush in brushes )
			{
				ImportedCollisionBrushContent outBrush = new ImportedCollisionBrushContent();

				outBrush.Shader = content.Shaders[brush.ShaderNum];

				for( int i = 0; i < brush.NumSides; i++ )
				{
					DBrushSide inSide = brushSides[brush.FirstSide + i];

					ImportedCollisionBrushSide side = new ImportedCollisionBrushSide();

					DPlane plane = planes[inSide.PlaneNumber];
					side.Plane = new Plane( plane.Normal, -plane.Distance );

					side.Shader = content.Shaders[inSide.ShaderNum];

					outBrush.Sides.Add( side );
				}

				content.CollisionBrushes.Add( outBrush );
			}

			Logger.LogMessage( "Imported {0} brushes.", brushes.Count );

			foreach( DLeaf leaf in leaves )
			{
				ImportedNodeContent outLeaf = new ImportedNodeContent();

				outLeaf.IsLeaf = true;

				if( leaf.Cluster >= 0 && leaf.Cluster < content.PvsData.Clusters.Count )
					outLeaf.Cluster = content.PvsData.Clusters[leaf.Cluster];
				outLeaf.Area = leaf.Area;

				outLeaf.BoundingBox = new BoundingBox( leaf.Mins, leaf.Maxs );

				for( int i = 0; i < leaf.NumLeafSurfaces; i++ )
					outLeaf.Surfaces.Add( content.Mesh.Geometry[markSurfaces[leaf.FirstLeafSurface + i]] );

				for( int i = 0; i < leaf.NumLeafBrushes; i++ )
				{
					int inBrushNum = leafBrushes[leaf.FirstLeafBrush + i];
					outLeaf.Brushes.Add( content.CollisionBrushes[inBrushNum] );
				}

				content.Leaves.Add( outLeaf );
			}

			Logger.LogMessage( "Imported {0} Leaves.", leaves.Count );

			//preallocate so we can link children as we get to them

			content.Nodes.Capacity = nodes.Count;
			for( int i = 0; i < nodes.Count; i++ )
				content.Nodes.Add( new ImportedNodeContent() );

			Logger.LogMessage( "Imported {0} Nodes.", nodes.Count );

			for( int i = 0; i < nodes.Count; i++ )
			{
				DNode node = nodes[i];
				ImportedNodeContent outNode = content.Nodes[i];

				outNode.IsLeaf = false;

				outNode.BoundingBox = new BoundingBox( node.Mins, node.Maxs );

				DPlane plane = planes[node.PlaneNumber];
				outNode.Plane = new Plane( plane.Normal, -plane.Distance );

				if( node.Child0 >= 0 )
					outNode.Child0 = content.Nodes[node.Child0];
				else
					outNode.Child0 = content.Leaves[-(node.Child0 + 1)];

				if( node.Child1 >= 0 )
					outNode.Child1 = content.Nodes[node.Child1];
				else
					outNode.Child1 = content.Leaves[-(node.Child1 + 1)];
			}
		}

		private void ImportPvs()
		{
			//load this directly since there's no nicer way
			PvsContent pvs = new PvsContent();

			LumpDesc desc = header[(int)Lumps.Visibility];

			if( desc.Length <= 8 )
				return;
			
			int numClusters = BitConverter.ToInt32( bspData, desc.Offset + 0 );
			int clusterBytes = BitConverter.ToInt32( bspData, desc.Offset + 4 );

			pvs.Clusters.Capacity = numClusters;
			for( int i = 0; i < numClusters; i++ )
			{
				PvsCluster clus = new PvsCluster();
				clus.ClusterNum = i;
				pvs.Clusters.Add( clus );
			}

			for( int i = 0; i < numClusters; i++ )
			{
				PvsCluster cluster = pvs.Clusters[i];

				int clusterOffset = desc.Offset + 8 + i * clusterBytes;
				for( int j = 0; j < numClusters; j++ )
				{
					if( (bspData[clusterOffset + (j >> 3)] & (1 << (j & 0x7))) != 0 )
						cluster.AddLink( pvs.Clusters[j] );
				}
			}

			content.PvsData = pvs;

			Logger.LogMessage( "Imported {0} pvs clusters.", pvs.Clusters.Count );
		}
	}
}
